import { NextRequest, NextResponse } from "next/server"; import * as fs from "fs/promises"; import * as path from "path"; import { eq } from "drizzle-orm"; import db from "@/db/db"; import { swpDocumentFiles } from "@/db/schema/SWP/swp-documents"; import { debugLog, debugError, debugSuccess } from "@/lib/debug-utils"; // API Route 설정 export const runtime = "nodejs"; export const maxDuration = 60; // 1분 타임아웃 /** * GET /api/swp/download/[fileId] * 파일 다운로드 (바이너리 직접 전송) */ export async function GET( request: NextRequest, { params }: { params: Promise<{ fileId: string }> } ) { try { const { fileId: fileIdString } = await params; const fileId = parseInt(fileIdString, 10); if (isNaN(fileId)) { return NextResponse.json( { success: false, error: "잘못된 파일 ID입니다." }, { status: 400 } ); } debugLog(`[download] 다운로드 시작`, { fileId }); // 1. 파일 정보 조회 const fileInfo = await db .select({ FILE_NM: swpDocumentFiles.FILE_NM, FLD_PATH: swpDocumentFiles.FLD_PATH, }) .from(swpDocumentFiles) .where(eq(swpDocumentFiles.id, fileId)) .limit(1); if (!fileInfo || fileInfo.length === 0) { debugError(`[download] 파일 정보 없음`, { fileId }); return NextResponse.json( { success: false, error: "파일 정보를 찾을 수 없습니다." }, { status: 404 } ); } const { FILE_NM, FLD_PATH } = fileInfo[0]; debugLog(`[download] 파일 정보 조회 완료`, { FILE_NM, FLD_PATH }); if (!FLD_PATH || !FILE_NM) { debugError(`[download] 파일 경로 또는 이름 없음`, { FILE_NM, FLD_PATH }); return NextResponse.json( { success: false, error: "파일 경로 또는 파일명이 없습니다." }, { status: 404 } ); } // 2. NFS 마운트 경로 확인 const nfsBasePath = process.env.SWP_MOUNT_DIR; if (!nfsBasePath) { debugError(`[download] SWP_MOUNT_DIR 환경변수가 설정되지 않았습니다.`); return NextResponse.json( { success: false, error: "서버 설정 오류: NFS 경로가 설정되지 않았습니다." }, { status: 500 } ); } // 3. 전체 파일 경로 생성 const normalizedFldPath = FLD_PATH.replace(/\\/g, '/'); const fullPath = path.join(nfsBasePath, normalizedFldPath, FILE_NM); debugLog(`[download] 파일 경로`, { fileId, FILE_NM, FLD_PATH, normalizedFldPath, fullPath, }); // 4. 파일 존재 여부 확인 try { await fs.access(fullPath, fs.constants.R_OK); } catch (accessError) { debugError(`[download] 파일 접근 불가`, { fullPath, error: accessError }); return NextResponse.json( { success: false, error: `파일을 찾을 수 없습니다: ${FILE_NM}` }, { status: 404 } ); } // 5. 파일 읽기 debugLog(`[download] 파일 읽기 시작`, { fullPath }); const fileBuffer = await fs.readFile(fullPath); debugLog(`[download] 파일 Buffer 읽기 완료`, { bufferLength: fileBuffer.length, isBuffer: Buffer.isBuffer(fileBuffer), bufferType: typeof fileBuffer, constructor: fileBuffer.constructor.name, first20Bytes: fileBuffer.slice(0, 20).toString('hex') }); // 6. MIME 타입 결정 const mimeType = getMimeType(FILE_NM); debugSuccess(`[download] 다운로드 성공`, { fileName: FILE_NM, size: fileBuffer.length, mimeType, }); // 7. 바이너리 응답 반환 return new NextResponse(fileBuffer, { status: 200, headers: { "Content-Type": mimeType, "Content-Disposition": `attachment; filename="${encodeURIComponent(FILE_NM)}"`, "Content-Length": String(fileBuffer.length), }, }); } catch (error) { console.error("[download] 오류:", error); debugError(`[download] 다운로드 실패`, { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }); return NextResponse.json( { success: false, error: error instanceof Error ? error.message : "파일 다운로드 실패", }, { status: 500 } ); } } /** * MIME 타입 결정 */ function getMimeType(fileName: string): string { const ext = path.extname(fileName).toLowerCase(); const mimeTypes: Record = { ".pdf": "application/pdf", ".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".xls": "application/vnd.ms-excel", ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".ppt": "application/vnd.ms-powerpoint", ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".txt": "text/plain", ".csv": "text/csv", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".zip": "application/zip", ".rar": "application/x-rar-compressed", ".7z": "application/x-7z-compressed", ".dwg": "application/acad", ".dxf": "application/dxf", }; return mimeTypes[ext] || "application/octet-stream"; }